package com.example.service;

import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.http.HttpUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.dao.SysPayDao;
import com.example.dto.PayCallBack;
import com.example.dto.PayRequest;
import com.example.dto.PayResponse;
import com.example.enums.SysPayStatus;
import com.example.handler.SysPayCallBackHandler;
import com.example.po.SysPay;
import com.example.po.SysUser;
import com.example.utils.ObjectMapperUtils;
import com.example.utils.RSAUtils;
import com.example.utils.TimeUtils;
import com.lly835.bestpay.enums.BestPayTypeEnum;
import com.lly835.bestpay.model.RefundRequest;
import com.lly835.bestpay.model.RefundResponse;
import com.lly835.bestpay.service.BestPayService;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public class SysPayServiceImpl extends ServiceImpl<SysPayDao, SysPay> implements SysPayService, InitializingBean, DisposableBean {

    Logger logger = LoggerFactory.getLogger(getClass());

    private static final String SQL = "select * from sys_lock where id = 1 for update";

    @Resource
    private SysPayCallBackHandler sysPayCallBackHandler;

    @Resource
    private BestPayService bestPayService;

    @Resource
    private SysUserService sysUserService;

    private volatile boolean isRunning = true;

    private Thread thread;

    private ExecutorService poolExecutor;

    @Value("${max.retry.count}")
    private int maxRetryCount;

    @Value("${max.retry.time}")
    private int maxRetryTime;

    @Resource
    private DataSource dataSource;

    /****
     * 发起支付并创建订单信息
     * @param sysPay
     * @return
     */
    @Override
    public com.lly835.bestpay.model.PayResponse apply(SysPay sysPay) {
        com.lly835.bestpay.model.PayRequest payRequest = new com.lly835.bestpay.model.PayRequest();
        payRequest.setPayTypeEnum(BestPayTypeEnum.getByName(sysPay.getType()));
        payRequest.setOrderId(sysPay.getOrderId());
        payRequest.setOrderAmount(Double.parseDouble(sysPay.getAmount()));
        payRequest.setOrderName(sysPay.getTitle());
        payRequest.setAttach(sysPay.getAttach());
        payRequest.setOpenid(sysPay.getOpenId());
        return bestPayService.pay(payRequest);
    }

    /****
     * 申请并返回内容
     * @param payRequest
     * @param httpServletRequest
     * @return
     * @throws Exception
     */
    @Override
    public PayResponse apply(PayRequest payRequest, HttpServletRequest httpServletRequest) throws Exception {
        SysUser sysUser = sysUserService.getById(payRequest.getUserId());
        Assert.isTrue(sysUser != null, "对应ip的账户信息不存在");
        SysPay sysPay = buildOrder(this.decryptRequest(payRequest, sysUser), sysUser);
        this.saveOrUpdate(sysPay);
        com.lly835.bestpay.model.PayResponse payResponse = this.apply(sysPay);
        return buildResponse(sysUser, sysPay, payResponse);
    }

    /****
     * 发起退款并设置信息
     * @param orderId
     * @return
     * @throws Exception
     */
    @Override
    public RefundResponse refund(String orderId) throws Exception {
        RefundRequest request = new RefundRequest();
        request.setOrderId(orderId);
        return bestPayService.refund(request);
    }

    /****
     * 支付回调
     * @param notifyData
     * @return
     * @throws Exception
     */
    @Override
    public String notify(String notifyData) {
        com.lly835.bestpay.model.PayResponse payResponse = bestPayService.asyncNotify(notifyData);
        setPayed(payResponse.getOrderId());
        return sysPayCallBackHandler.callBack(payResponse);
    }

    /****
     * 设置支付状态
     * @param orderId
     */
    @Override
    public void setPayed(String orderId) {
        SysPay sysPay = getSysPay(orderId);
        if (sysPay != null && !sysPay.isPayed()) {
            sysPay.setPayed(true);
            sysPay.setPayTime(new Date());
            updateById(sysPay);
        }
    }

    /*****
     * 查询订单
     * @param orderId
     * @return
     */
    @Override
    public SysPay getSysPay(String orderId) {
        return getBaseMapper().getSysPay(orderId);
    }

    /***
     * 发送列表
     * @return
     */
    @Override
    public List<SysPay> getSysPayList() {
        return getBaseMapper().getSysPayList(maxRetryCount);
    }

    /***
     * 解密参数信息
     * @param payRequest
     * @param sysUser
     * @return
     * @throws Exception
     */
    @Override
    public PayRequest decryptRequest(PayRequest payRequest, SysUser sysUser) throws Exception {
        RSA rsa = new RSA(sysUser.getPriKey(), sysUser.getPubKey());
        PayRequest decryptPayRequest = new PayRequest();
        decryptPayRequest.setOrderId(RSAUtils.decrypt(rsa, payRequest.getOrderId()));
        decryptPayRequest.setType(RSAUtils.decrypt(rsa, payRequest.getType()));
        decryptPayRequest.setAmount(RSAUtils.decrypt(rsa, payRequest.getAmount()));
        decryptPayRequest.setAttach(RSAUtils.decrypt(rsa, payRequest.getAttach()));
        decryptPayRequest.setOpenId(RSAUtils.decrypt(rsa, payRequest.getOpenId()));
        decryptPayRequest.setUserId(payRequest.getUserId());
        return decryptPayRequest;
    }

    /****
     * 构建订单信息
     * @param payRequest
     * @return
     */
    @Override
    public SysPay buildOrder(PayRequest payRequest, SysUser sysUser) throws Exception {
        SysPay sysPay = new SysPay();
        Assert.isTrue(!StringUtils.isEmpty(payRequest.getOrderId()), "订单编号不可以为空");
        Assert.isTrue(!StringUtils.isEmpty(payRequest.getType()), "支付类型不可以为空");
        Assert.isTrue(Long.parseLong(payRequest.getAmount()) > 0, "支付金额必填且是长整型且大于0");
        BeanUtils.copyProperties(payRequest, sysPay);
        sysPay.setUserId(sysUser.getId());
        sysPay.recalculateFields();
        return sysPay;
    }


    /****
     * 封装结果并返回
     * @param sysUser
     * @param sysPay
     * @param payResponse
     * @return
     * @throws Exception
     */
    @Override
    public PayResponse buildResponse(SysUser sysUser, SysPay sysPay, com.lly835.bestpay.model.PayResponse payResponse) throws Exception {
        RSA rsa = new RSA(sysUser.getPriKey(), sysUser.getPubKey());
        String data = ObjectMapperUtils.getDefaultObjectMapper().writeValueAsString(payResponse);
        return PayResponse.ok(RSAUtils.encrypt(rsa, data), RSAUtils.sign(data, rsa.getPrivateKey()));
    }

    /****
     * 初始化并启动工作线程
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        SysPayService sysPayService = this;
        this.poolExecutor = Executors.newCachedThreadPool();
        this.thread = new Thread(new Runnable() {
            @Override
            public void run() {
                logger.warn("pay callback thread start ...");
                while (isRunning) {
                    Connection conn = null;
                    PreparedStatement preparedStatement = null;
                    boolean connAutoCommit = false;
                    try {
                        conn = dataSource.getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        conn.setAutoCommit(false);
                        preparedStatement = conn.prepareStatement(SQL);
                        preparedStatement.execute();

                        List<SysPay> sysPayList = getSysPayList();
                        if (!sysPayList.isEmpty()) {
                            CountDownLatch countDownLatch = new CountDownLatch(sysPayList.size());
                            for (SysPay sysPay : sysPayList) {
                                poolExecutor.execute(new PushThread(sysPayService, sysUserService, sysPay, countDownLatch));
                            }
                            try {
                                countDownLatch.await();
                            } catch (InterruptedException e) {
                                logger.warn("countDownLatch interrupted ..." + e.getMessage());
                            }
                        }
                    } catch (Exception e) {
                        logger.warn("线程处理出错: " + e.getMessage());
                    } finally {
                        if (conn != null) {
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                            }
                        }
                        try {
                            if (conn != null) {
                                conn.setAutoCommit(connAutoCommit);
                            }
                        } catch (SQLException e) {
                        }
                        if (conn != null) {
                            try {
                                conn.close();
                            } catch (SQLException e) {
                            }
                        }
                        if (null != preparedStatement) {
                            try {
                                preparedStatement.close();
                            } catch (SQLException e) {
                            }
                        }
                    }
                }
            }
        });
        this.thread.start();
    }

    /***
     * 销毁线程池回收资源
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        this.isRunning = false;
        if (this.thread != null && this.thread.isAlive()) {
            this.thread.interrupt();
        }
        this.poolExecutor.shutdownNow();
    }

    /****
     * 回调处理程序
     */
    @Data
    class PushThread implements Runnable {

        private SysUserService sysUserService;

        private SysPayService sysPayService;

        private SysPay sysPay;

        private CountDownLatch countDownLatch;

        public PushThread(SysPayService sysPayService, SysUserService sysUserService, SysPay sysPay, CountDownLatch countDownLatch) {
            this.sysPayService = sysPayService;
            this.sysUserService = sysUserService;
            this.sysPay = sysPay;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            boolean flag = true;
            Map<String, Object> map = new HashMap();
            try {
                SysUser sysUser = sysUserService.getById(sysPay.getUserId());
                RSA rsa = new RSA(sysUser.getPriKey(), sysUser.getPubKey());
                PayCallBack payCallBack = new PayCallBack();
                BeanUtils.copyProperties(sysPay, payCallBack);
                String data = ObjectMapperUtils.getDefaultObjectMapper().writeValueAsString(payCallBack);
                String sign = RSAUtils.sign(data, rsa.getPrivateKey());
                map.put("data", RSAUtils.encrypt(rsa, data));
                map.put("sign", sign);
                String result = HttpUtil.post(sysUser.getUrl(), map);
                if (!StringUtils.isEmpty(result) && RSAUtils.decrypt(rsa, result).equalsIgnoreCase("SUCCESS")) {
                    sysPay.setStatus(SysPayStatus.SUCCESS);
                } else {
                    flag = false;
                }
            } catch (Exception e) {
                flag = false;
                sysPay.setError(e.getMessage());
            }
            if (!flag && sysPay.getRetryCount() < maxRetryCount) {
                int count = sysPay.getRetryCount() + 1;
                Date nextTime = TimeUtils.nowAddMinute(count * maxRetryTime);
                sysPay.setRetryCount(count);
                sysPay.setRetryTime(nextTime);
            }
            sysPayService.saveOrUpdate(sysPay);
            countDownLatch.countDown();
        }
    }
}
